﻿package com.janisRadins.asDraw {
	import com.janisRadins.asDraw.svg.PathSegment;
	import flash.geom.Point;
	import mx.utils.ObjectUtil;
	/**
	 * Class which deals with turning SVG Path element "d" string property into list of Curve elements stuffed into Path instance
	 * @author Jānis Radiņš
	 */
	final public class SVGPathParser {
		private static var instance:SVGPathParser = null;
		/**
		 * Method that takes string Path definition and return ICurve instance with parsed commands
		 * @param	data	String with Path commands in same format as "d" attribute of SVG Path element
		 * @return	instance of ICurve, Path or PathCollection if data has more than on moveTo command
		 */
		public static function parse(data:String):ICurve {
			if (instance == null) {
				instance = new SVGPathParser();
			}
			return instance.doParseData(data);
		}
		/**
		 * String input data of SVG path element "d" attribute to parse
		 */
		private var svg_data:String = null;
		/**
		 * Vector with parsed drawing commands
		 */
		private var commands:Vector.<PathSegment> = null;
		/**
		 * Current Path segment to parse
		 */
		private var segment:PathSegment = null;
		/**
		 * Previouse Path segment
		 */
		private var prevSegment:PathSegment = null;
		/**
		 * Variable which holds value of coordinate system currently in use
		 */
		private var currentSegmentIsAbs:Boolean;
		/**
		 * Collection of parsed Path's - Paths count > 0 happens when <code>svg_data</code> has more than one M command
		 */
		private var pathsCollection:PathCollection = null;
		/**
		 * Current path that is developed from commands
		 */
		private var _currentPath:Path = null;
		
		/**
		 * Constructor which does nothing
		 */
		public function SVGPathParser() {
			
		}
		/**
		 * Method that takes string Path definition and return ICurve instance with parsed commands
		 * @param	data	String with Path commands in same format as "d" attribute of SVG Path element
		 * @return	instance of ICurve, Path or PathCollection if data has more than on moveTo command
		 */
		private function doParseData(data:String):ICurve {
			svg_data = data;
			parseCommands();
			if (commands.length == 0) {
				throw new Error("No commands found in Path data");
				return null;
			}
			// Initiate variables
			pathsCollection = new PathCollection();
			_currentPath = new Path();
			// Loop through commands and invoke associated parser methods
			for (var i:uint = 0; i < commands.length; i++ ) {
				if (i > 0) {
					prevSegment = segment;
				}
				segment = commands[i];
				currentSegmentIsAbs = segment.absCoords;
				switch(segment.pathSegType) {
					case PathSegment.PATHSEG_CLOSEPATH  :
						closepath();
						break;
					case PathSegment.PATHSEG_MOVETO_ABS :
					case PathSegment.PATHSEG_MOVETO_REL : 
						moveTo();
						break;
					case PathSegment.PATHSEG_LINETO_ABS : 
					case PathSegment.PATHSEG_LINETO_REL : 
						lineto();
						break;
					case PathSegment.PATHSEG_CURVETO_CUBIC_ABS :
					case PathSegment.PATHSEG_CURVETO_CUBIC_REL :
						curveto();
						break;
					case PathSegment.PATHSEG_CURVETO_QUADRATIC_ABS :
					case PathSegment.PATHSEG_CURVETO_QUADRATIC_REL :
						quadraticBezierCurveto();
						break;
					case PathSegment.PATHSEG_ARC_ABS :
					case PathSegment.PATHSEG_ARC_REL :
						ellipticalArc();
						break;
					case PathSegment.PATHSEG_LINETO_HORIZONTAL_ABS :
					case PathSegment.PATHSEG_LINETO_HORIZONTAL_REL :
						horizontalLineto();
						break;
					case PathSegment.PATHSEG_LINETO_VERTICAL_ABS :
					case PathSegment.PATHSEG_LINETO_VERTICAL_REL :
						verticalLineto();
						break;
					case PathSegment.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS :
					case PathSegment.PATHSEG_CURVETO_CUBIC_SMOOTH_REL :
						smoothCurveto();
						break;
					case PathSegment.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS : 
					case PathSegment.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL : 
						smoothQuadraticBezierCurveto();
						break;
					default :
						throw new Error("Unparsable segment data " + segment);
				}
			}
			// If _currentPath is only Path parsed, return it
			if (_currentPath != null && pathsCollection.pathCount == 0) {
				return _currentPath;
			}
			// Else add it to collection
			if (_currentPath != null) {
				pathsCollection.push(_currentPath)
			}
			// Return Path collection
			return pathsCollection;
		}
		/**
		 * Method that turns string path definiton ("d" attribute) into Vector with PathSegment instances for later parsing
		 */
		private function parseCommands():void {
			commands = new Vector.<PathSegment>();
			// Split string definiton into chunks of command definitons
			var str_cmd:Array = svg_data.match(/[a-z][^a-z]*/ig);
			var data:Array;
			var type:uint;
			// Loop through data chunks and turn them into PathSegment instances for later parsing
			for each(var cmd:String in str_cmd) {
				cmd = cmd.replace(/^\s+|\s+$/g, "");
				data = cmd.substr(1).replace(/^\s+|\s+$/g, "").split(/[^\d.-]+/);
				type = PathSegment.literalTypeToPathType(cmd.charAt(0));
				if (type == PathSegment.PATHSEG_UNKNOWN) {
					throw new Error("Unknown path segment type");
				}
				/**
				 * According to SVG specification: "If a moveto is followed by multiple pairs of coordinates, the subsequent 
				 * pairs are treated as implicit lineto commands."
				 * Therefor if this condition turns to be true this bastid must be split into two PathSegments
				 */
				if (type == PathSegment.PATHSEG_MOVETO_ABS || type == PathSegment.PATHSEG_MOVETO_REL && data.length > 2) {
					var moveCMD:PathSegment = new PathSegment([data.shift(), data.shift()], null, type);
					var lineToCMD:PathSegment = new PathSegment(data, null, moveCMD.absCoords ? PathSegment.PATHSEG_LINETO_ABS : PathSegment.PATHSEG_LINETO_REL);
					commands.push(moveCMD, lineToCMD);
				} else {
					commands.push(new PathSegment(data, null, type));
				}
			}
		}
		/**
		 * Lil helper function that helps to deal with current path instance
		 */
		private function get currentPath():Path {
			if (_currentPath == null) {
				_currentPath = new Path();
			}
			return _currentPath;
		}
		/**
		 * Previouse Path instance accessor
		 */
		private function get prevPath():Path {
			if (pathsCollection.pathCount > 0) {
				return pathsCollection.getPathAt( -1);
			}
			return null;
		}
		/**
		 * Method that removes current path from active reach and stores it in collection
		 */
		private function getNextPath():void {
			if(_currentPath != null) {
				pathsCollection.push(_currentPath);
				_currentPath = null;
			}
		}
		/**
		 * moveTo command parser
		 */
		private function moveTo():void {
			if (currentPath.curveCount == 0) {
				// M as first command is always in absolute coordinates
				currentPath.x = segment.data[0];
				currentPath.y = segment.data[1];
			} else {
				getNextPath();
				if (segment.absCoords) {
					currentPath.x = segment.data[0];
					currentPath.y = segment.data[1];
				} else {
					currentPath.x = prevPath.pathClosePoint.x + segment.data[0];
					currentPath.y = prevPath.pathClosePoint.y + segment.data[1];
				}
			}
		}
		/**
		 * Move current path pen back to Path starting point and close path
		 */
		private function closepath():void {
			currentPath.curveTo(NaN, NaN, currentPath.x - currentPath.pathClosePoint.x, currentPath.y - currentPath.pathClosePoint.y);
			getNextPath();
		}
		/**
		 * lineTo command parser method
		 */
		private function lineto():void {
			var i:uint;
			if (segment.absCoords) {
				for (i = 0; i < segment.data.length; i += 2) {
					currentPath.curveTo(NaN, NaN, segment.data[i] - currentPath.pathClosePoint.x, segment.data[i + 1] - currentPath.pathClosePoint.y);
				}
			} else {
				for (i = 0; i < segment.data.length; i += 2) {
					currentPath.curveTo(NaN, NaN, segment.data[i], segment.data[i + 1]);
				}
			}
		}
		/**
		 * horizontalLineto command parser method
		 */
		private function horizontalLineto():void {
			if (segment.absCoords) {	// If data contains absolute values add last value from array
				currentPath.curveTo(NaN, NaN, segment.data[segment.data.length - 1] - currentPath.pathClosePoint.x);
			} else {					// Else, sum up ap all relative values
				var length:Number = 0;
				for each(var seg:Number in segment.data) {
					length += seg;
				}
				currentPath.curveTo(NaN, NaN, length);
			}
		}
		/**
		 * verticalLineto command parser method
		 */
		private function verticalLineto():void {
			if (segment.absCoords) {	// If data contains absolute values add last value from array
				currentPath.curveTo(NaN, NaN, 0, segment.data[segment.data.length - 1] - currentPath.pathClosePoint.y);
			} else {					// Else, sum up ap all relative values
				var length:Number = 0;
				for each(var seg:Number in segment.data) {
					length += seg;
				}
				currentPath.curveTo(NaN, NaN, 0, length);
			}
		}
		/**
		 * curveto command parser method
		 */
		private function curveto():void {
			var cubicCurve:CubicCurve;
			for (var i:uint = 0; i < segment.data.length; i += 6) {
				cubicCurve = new CubicCurve(
					segment.absCoords ? currentPath.pathClosePoint.x : 0,
					segment.absCoords ? currentPath.pathClosePoint.y : 0,
					segment.data[i],
					segment.data[i+1],
					segment.data[i+2],
					segment.data[i+3],
					segment.data[i+4],
					segment.data[i+5]
				);
				// Usualy when order of data if of significance for each is not good idea, but fuck it
				for each(var curveData:Array in cubicCurve.quadratics) {
					currentPath.curveTo(
						curveData[2] - curveData[0], 
						curveData[3] - curveData[1], 
						curveData[4] - curveData[0], 
						curveData[5] - curveData[1]
					);
				}
			}
		}
		/**
		 * smoothCurveto command parser method
		 */
		private function smoothCurveto():void {
			var cubicCurve:CubicCurve;
			var prevAnchor:Point = new Point();
			if (prevSegment != null && new Array(
				PathSegment.PATHSEG_CURVETO_CUBIC_ABS,
				PathSegment.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS,
				PathSegment.PATHSEG_CURVETO_CUBIC_REL,
				PathSegment.PATHSEG_CURVETO_CUBIC_SMOOTH_REL
			).indexOf(prevSegment.pathSegType) != -1) {
				prevAnchor = new Point(
					prevSegment.data[prevSegment.data.length - 2] - prevSegment.data[prevSegment.data.length - 4],
					prevSegment.data[prevSegment.data.length - 1] - prevSegment.data[prevSegment.data.length - 3]
				);
			}
			for (var i:uint = 0; i < segment.data.length; i += 4) {
				if (segment.absCoords) {
					cubicCurve = new CubicCurve(
						currentPath.pathClosePoint.x, currentPath.pathClosePoint.y,
						currentPath.pathClosePoint.x + prevAnchor.x, currentPath.pathClosePoint.y + prevAnchor.y,
						segment.data[i], segment.data[i+1],
						segment.data[i+2], segment.data[i+3]
					);
				} else {
					cubicCurve = new CubicCurve(
						0, 0,
						prevAnchor.x, prevAnchor.y,
						segment.data[i], segment.data[i+1],
						segment.data[i+2], segment.data[i+3]
					);
				}
				for each(var curveData:Array in cubicCurve.quadratics) {
					currentPath.curveTo(
						curveData[2] - curveData[0], 
						curveData[3] - curveData[1], 
						curveData[4] - curveData[0], 
						curveData[5] - curveData[1]
					);
				}
				prevAnchor.x = cubicCurve.c2x - cubicCurve.p2x;
				prevAnchor.y = cubicCurve.c2y - cubicCurve.p2y;
			}
		}
		/**
		 * smoothCurveto command parser method
		 */
		private function quadraticBezierCurveto():void {
			var control:Point;
			var anchor:Point;
			for (var i:uint = 0; i < segment.data.length; i += 4) {
				control = new Point(segment.data[i], segment.data[i + 1]);
				anchor = new Point(segment.data[i + 2], segment.data[i + 3]);
				if (segment.absCoords) {
					control = control.subtract(currentPath.pathClosePoint);
					anchor = anchor.subtract(currentPath.pathClosePoint);
				}
				currentPath.curveTo(control.x, control.y, anchor.x, anchor.y);
			}
		}
		/**
		 * smoothQuadraticBezierCurveto command parser method
		 */
		private function smoothQuadraticBezierCurveto():void {
			var prevAnchor:Point = new Point();
			if (prevSegment != null && new Array(
				PathSegment.PATHSEG_CURVETO_QUADRATIC_ABS,
				PathSegment.PATHSEG_CURVETO_QUADRATIC_REL,
				PathSegment.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS,
				PathSegment.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL
			).indexOf(prevSegment.pathSegType) != -1) {
				prevAnchor = new Point(
					prevSegment.data[prevSegment.data.length - 2] - prevSegment.data[prevSegment.data.length - 4],
					prevSegment.data[prevSegment.data.length - 1] - prevSegment.data[prevSegment.data.length - 3]
				);
			}
			var anchorPoint:Point = new Point();
			for (var i:uint = 0; i < segment.data.length; i += 2) {
				if (segment.absCoords == true) {
					anchorPoint.x = segment.data[i] - currentPath.pathClosePoint.x;
					anchorPoint.y = segment.data[i + 1] - currentPath.pathClosePoint.y;
				} else {
					anchorPoint.x = segment.data[i];
					anchorPoint.y = segment.data[i + 1];
				}
				currentPath.curveTo(prevAnchor.x, prevAnchor.y, anchorPoint.x, anchorPoint.y);
				prevAnchor.x = anchorPoint.x - prevAnchor.x;
				prevAnchor.y = anchorPoint.y - prevAnchor.y;
			}
		}
		/**
		 * Elliptic arc command parser function
		 */
		private function ellipticalArc():void {
			var closePoint:Point = new Point();
			for (var i:uint = 0; i < segment.data.length; i += 7) {
				closePoint.x = segment.data[5];
				closePoint.y = segment.data[6];
				if (segment.absCoords) {
					closePoint = closePoint.subtract(currentPath.pathClosePoint);
				}
				ellipticalArc_imp(segment.data[0], segment.data[1], segment.data[2], segment.data[3] == "1", segment.data[4] == "1", closePoint.x, closePoint.y);
			}
		}
		/**
		 * Elliptic arc command implementation
		 * @param	rx				Ellipse radius X
		 * @param	ry				Ellipse radius Y
		 * @param	xAxisRotation	Ellipse x axis rotation
		 * @param	largeArcFlag	Indicates whether ellipse sector that is larger than 2π should be used
		 * @param	sweepFlag		Determines if the arc will be drawn in a "positive-angle" direction
		 * @param	x				End point x in relative coordinates to current pen position
		 * @param	y				End point y in relative coordinates to current pen position
		 */
		private function ellipticalArc_imp(rx:Number, ry:Number, xAxisRotation:Number, largeArcFlag:Boolean, sweepFlag:Boolean, x:Number, y:Number):void {
			var data:Object = computeSvgArc(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, 0, 0, x, y);
			var ellipseData:Array = DrawUtils.getEllipse(
				data.radius,
				data.yRadius,
				DrawUtils.d2r(data.startAngle - data.arc),
				DrawUtils.d2r(data.startAngle),
				sweepFlag,
				DrawUtils.d2r(data.xAxisRotation)
			);
			var ep:Point = new Point(ellipseData[0], ellipseData[1]);
			for (var i:uint = 2; i < ellipseData.length; i += 4) {
				currentPath.curveTo(
					ellipseData[i] - ep.x,
					ellipseData[i + 1] - ep.y,
					ellipseData[i + 2] - ep.x,
					ellipseData[i + 3] - ep.y
				);
				ep.x = ellipseData[i + 2];
				ep.y = ellipseData[i + 3];
			}
		}
		/** 
		 * Function from degrafa
		 * com.degrafa.geometry.utilities.ArcUtils
		 */
		private function computeSvgArc(rx:Number, ry:Number, angle:Number, largeArcFlag:Boolean, sweepFlag:Boolean, x:Number,y:Number, LastPointX:Number, LastPointY:Number):Object {
	        //store before we do anything with it	 
	        var xAxisRotation:Number = angle;	 
	        // Compute the half distance between the current and the final point
	        var dx2:Number = (LastPointX - x) / 2;
	        var dy2:Number = (LastPointY - y) / 2;
	        // Convert angle from degrees to radians
	        angle = DrawUtils.d2r(angle);
	        var cosAngle:Number = Math.cos(angle);
	        var sinAngle:Number = Math.sin(angle);
	        //Compute (x1, y1)
	        var x1:Number = (cosAngle * dx2 + sinAngle * dy2);
	        var y1:Number = (-sinAngle * dx2 + cosAngle * dy2);
	        // Ensure radii are large enough
	        rx = Math.abs(rx);
	        ry = Math.abs(ry);
	        var Prx:Number = rx * rx;
	        var Pry:Number = ry * ry;
	        var Px1:Number = x1 * x1;
	        var Py1:Number = y1 * y1;
	        // check that radii are large enough
	        var radiiCheck:Number = Px1 / Prx + Py1 / Pry;
	        if (radiiCheck > 1) {
	            rx = Math.sqrt(radiiCheck) * rx;
	            ry = Math.sqrt(radiiCheck) * ry;
	            Prx = rx * rx;
	            Pry = ry * ry;
	        }
	        //Compute (cx1, cy1)
	        var sign:Number = (largeArcFlag == sweepFlag) ? -1 : 1;
	        var sq:Number = ((Prx * Pry) - (Prx * Py1) - (Pry * Px1)) / ((Prx * Py1) + (Pry * Px1));
	        sq = (sq < 0) ? 0 : sq;
	        var coef:Number = (sign * Math.sqrt(sq));
	        var cx1:Number = coef * ((rx * y1) / ry);
	        var cy1:Number = coef * -((ry * x1) / rx);
	        //Compute (cx, cy) from (cx1, cy1)
	        var sx2:Number = (LastPointX + x) / 2;
	        var sy2:Number = (LastPointY + y) / 2;
	        var cx:Number = sx2 + (cosAngle * cx1 - sinAngle * cy1);
	        var cy:Number = sy2 + (sinAngle * cx1 + cosAngle * cy1);
	        //Compute the angleStart (angle1) and the angleExtent (dangle)
	        var ux:Number = (x1 - cx1) / rx;
	        var uy:Number = (y1 - cy1) / ry;
	        var vx:Number = (-x1 - cx1) / rx;
	        var vy:Number = (-y1 - cy1) / ry;
	        var p:Number;
	        var n:Number;
	        //Compute the angle start
	        n = Math.sqrt((ux * ux) + (uy * uy));
	        p = ux;
	        sign = uy < 0 ? -1 : 1;
	        var angleStart:Number = DrawUtils.r2d(sign * Math.acos(p / n));
	        // Compute the angle extent
	        n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
	        p = ux * vx + uy * vy;
	        sign = ux * vy - uy * vx < 0 ? -1.0 : 1.0;
	        var angleExtent:Number = DrawUtils.r2d(sign * Math.acos(p / n));
	        if(!sweepFlag && angleExtent > 0) {
	            angleExtent -= 360;
	        } else if (sweepFlag && angleExtent < 0) {
	            angleExtent += 360;
	        }
	        angleExtent %= 360;
	        angleStart %= 360;
			return Object({x:LastPointX, y:LastPointY, startAngle:angleStart, arc:angleExtent, radius:rx, yRadius:ry, xAxisRotation:xAxisRotation, cx:cx, cy:cy});
	    }
	}
}